﻿using System;
using System.Collections.Generic;
using System.Text;
using Madason.MetaModel;
using System.Xml;

namespace Madason.Templates.Schema
{
    /// <summary>
    /// Generate a Schema for a Schema-Class def
    /// </summary>
    public class SchemaGenerator
    {
        public const string NAMESPACE_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";

        Class schemaClass = null;
        SchemaTypeMapper typeMapper = null;
        Dictionary<string, string> prefixMap = new Dictionary<string, string>();


        /// <summary>
        /// Ctor
        /// </summary>
        /// <param name="schemaClass"></param>
        public SchemaGenerator(Class schemaClass)
        {
            this.schemaClass = schemaClass;
            this.typeMapper = new SchemaTypeMapper();
        }


        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="schemaClass"></param>
        /// <param name="typeMapper"></param>
        public SchemaGenerator(Class schemaClass, SchemaTypeMapper typeMapper)
        {
            this.schemaClass = schemaClass;
            this.typeMapper = typeMapper;
        }




        /// <summary>
        /// Generate a single element / class
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="cls"></param>
        public void GenerateComplexType(XmlWriter writer, Class cls)
        {                        
            writer.WriteStartElement("xs", "complexType", NAMESPACE_XML_SCHEMA);
            writer.WriteAttributeString("name", cls.Name);
            writer.WriteStartElement("xs", "sequence", NAMESPACE_XML_SCHEMA);

            foreach (Madason.MetaModel.Attribute attr in cls.Attributes)
            {                
                writer.WriteStartElement("xs", "element", NAMESPACE_XML_SCHEMA);
                writer.WriteAttributeString("name", attr.Name);

                string prefix = prefixMap.ContainsKey(attr.Type.Id) ? prefixMap[attr.Type.Id] + ":" : "";
                if (typeMapper.IsSubElement(attr.Type))
                    writer.WriteAttributeString("type", prefix + attr.Type.Name);
                else
                    writer.WriteAttributeString("type", typeMapper.GetSchemaType(attr.Type));                
                writer.WriteEndElement();
            }

            writer.WriteEndElement(); // sequence
            writer.WriteEndElement(); // complexType
        }


        public void GenerateElement(XmlWriter writer, Class cls)
        {
            writer.WriteStartElement("xs", "element", NAMESPACE_XML_SCHEMA);
            writer.WriteAttributeString("name", cls.Name);
            writer.WriteAttributeString("type", cls.Name);
            writer.WriteEndElement();
        }



        /// <summary>
        /// searches all classes used as attribute hirachically
        /// </summary>
        /// <param name="cls"></param>
        /// <param name="dict"></param>
        private void AddDependendClasses(Class cls, Dictionary<string, Class> dict)
        {
            dict.Add(cls.Id, cls);
            foreach (Madason.MetaModel.Attribute attr in cls.Attributes)
            {
                if (typeMapper.IsSubElement(attr.Type))
                {
                    AddDependendClasses(attr.Type, dict);
                }
            }
        }


        /// <summary>
        /// check the class is contained by another schema
        /// </summary>
        /// <param name="cls"></param>
        /// <returns></returns>
        private bool IsClassExternal(Class cls)
        {
            bool external = false;
            foreach (Association assoc in cls.Associations)
            {
                // does the class belong to another schema?
                if (assoc.TargetClass.StereotypeName == "Schema" &&
                    assoc.TargetClass.Id != schemaClass.Id)
                {
                    external = true;
                }
            }
            return external;
        }

        private Class FindSchemaClass(Class cls)
        {
            foreach (Association assoc in cls.Associations)
            {
                // does the class belong to another schema?
                if (assoc.TargetClass.StereotypeName == "Schema")
                {
                    return assoc.TargetClass;
                }
            }
            return null;
        }


        /// <summary>
        /// generate a schema
        /// </summary>
        /// <param name="writer"></param>
        public void Generate(XmlWriter writer)
        {
            Dictionary<string, Class> classesToGenerate = new Dictionary<string, Class>();
            foreach (Association assoc in schemaClass.Associations)
            {
                Class cls = assoc.TargetClass;
                AddDependendClasses(cls, classesToGenerate);
            }

            
            // find imports
            Dictionary<string, Class> imports = new Dictionary<string, Class>();
            foreach (Class cls in classesToGenerate.Values)
            {
                if (IsClassExternal(cls) && !imports.ContainsKey(cls.Id))
                    imports.Add(cls.Id, cls);
            }

            // gen schema element
            writer.WriteStartElement("xs", "schema", NAMESPACE_XML_SCHEMA);
            writer.WriteAttributeString("targetNamespace", schemaClass.TaggedValues["TargetNamespace"]);
            int i = 0;
            foreach (Class cls in imports.Values)
            {
                string prefix = "ns"+i;
                i++;
                prefixMap[cls.Id] = prefix;
                writer.WriteAttributeString("xmlns:" + prefix, FindSchemaClass(cls).TaggedValues["TargetNamespace"]);
            }



            // generates imports
            foreach (Class cls in imports.Values)
            {
                writer.WriteStartElement("xs", "import", NAMESPACE_XML_SCHEMA);
                writer.WriteAttributeString("schemaLocation", "./" + cls.Name + ".xsd");
                writer.WriteAttributeString("namespace", FindSchemaClass(cls).TaggedValues["TargetNamespace"]);
            }

            // generates complex types
            foreach (Class cls in classesToGenerate.Values)
            {
                if (!IsClassExternal(cls))
                    GenerateComplexType(writer, cls);
            }

            // generate elements
            foreach (Association assoc in schemaClass.Associations)
            {
                GenerateElement(writer, assoc.OtherEnd.Class);
            }

            writer.WriteEndElement();
        }
    }
}
